# $Id: scm.py 33b6870bbab4 2012/09/09 20:06:34 pgurumur $
# Copyright (c) 2009 Prabhu Gurumurthy <pgurumur@gmail.com>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#       

__version__  = "$Revision: 33b6870bbab4 $"
__author__   = "$Author: pgurumur $"
__modified__ = "$Date: 2012-09-09 20:06:34Z $"

import datetime
import os
import shutil
import tempfile

import pysvn
from mercurial import commands, cmdutil, ui, hg, error, util

import log
import stdlib

from exception import SVNErrorNotWC, SVNErrorAuthFailed, SVNErrorNotInRepo
from exception import SVNErrorMethodNotAllowed, SVNAllreadyControlled
from exception import SCMError

class List(object):
   def __init__(self, **keywords):
      self._author = None
      self._rev = 0
      self._props = 0
      self._time = None
      self._path = None
      self._size = 0

      if "last_author" in keywords:
         self._author = keywords["last_author"]

      if "created_rev" in keywords:
         self._rev = keywords["created_rev"].number

      if "has_props" in keywords:
         self._props = keywords["has_props"]

      if "time" in keywords:
         self._time = str(datetime.datetime.fromtimestamp(keywords["time"]))

      if "path" in keywords:
         self._path = keywords["path"]

      if "size" in keywords:
         self._size = keywords["size"]

   def __str__(self):
      return str(self._path).encode('utf-8')

   def __repr__(self):
      return repr(str(self))

   def __unicode__(self):
      return unicode(str(self))

   size = property(fget = lambda self: int(self._size))
   date = property(fget = lambda self: self._time)
   author = property(fget = lambda self: self._author)

# Key value pairs of errors that can be encountered by svn
SVNErrors = {
      150002 : SVNAllreadyControlled,

      # Path is not a working copy
      155007 : SVNErrorNotWC,

      # Authorisation failed
      170001 : SVNErrorAuthFailed,

      # url not exists
      170000 : SVNErrorNotInRepo,
      # method not allowed -already in repo when mkdir
      175002 : SVNErrorMethodNotAllowed
      }

def handle(function, *args, **keywords):
   if function:
      try:
         return function(*args, **keywords)
      except KeyboardInterrupt:
         pass
      except pysvn.ClientError, err:
         for message, code in err.args[1]:
            message += '\n' + str(args) + str(keywords)
            if code in SVNErrors:
               raise SVNErrors[code], message
            else:
               raise

def GetWorkingDir():
   tmpdir = tempfile.gettempdir()
   path = os.path.join(tmpdir, os.path(__file__))
   if not os.path.isdir(path):
      try:
         os.makedirs(path)
         print "creating SVN working copy directory: %s" %path
      except (IOError, OSError):
         raise

   return path

def Rebuild(URL, **Credentials):
   path = GetWorkingDir()
   shutil.rmtree(path)
   path = GetWorkingDir()
   scm = None
   try:
      svn = scm(Credentials)
   except SVNError:
      raise
   else:
      svn.checkout(path, URL)

class SVNWorkingCopy(object):
   def __init__(self, Name, URL, **Credentials):
      if url is None or len(url) == 0:
         raise ValueError("SVN url is not defined.")

      self._name = Name
      self._isNew = False
      self._svn = None
      try:
         self._svn = scm(Credentials)
      except SCMError as err:
         self._error(err)

      temp = GetWorkingDir()
      destPath = os.path.join(temp, Name)
      destURL = "/".join((URL, Name))

      if not os.path.exists(destPath):
         self._svn.WorkingCopy(destPath, destURL)

      # now for a book file
      self._path = os.path.join(destPath, Name)
      if not os.path.exists(self._path):
          self._isNew = True

   def GetRevision(self, Content = None):
      rev = None
      idLine = None

      if Content:
         try:
            idLine = Content.split("\n")[0]
         except:
            idLine = Content
      elif self._path:
         if os.path.exists(self._path):
            fh = open(self._path)
            idLine = fh.readline().strip()
            fh.close()

      try:
         rev = idLine.split(" ")[2]
      except:
         self._error("cannot parse SVN id string - format incorrect: " + idLine)

      return rev

   def _error(self, Message):
      if Message:
         raise ValueError(Message)

   def CompareRevisions(self, Content):
      mesg = None
      rev = self.GetRevision(Content)
      wcrev = self.GetRevision()

      if rev != wcrev:
         mesg = "Cannot update content with an older or unversion edition. "
         mesg += "Download the recent version before making changes."
         mesg += "\nActual revsion: %s, uploaded %s" %(wcrev, rev)

      return mesg

   def CheckIn(self, content, Comment):
      if self._isNew or not content.startswith("$"):
         content += "$Id: scm.py 33b6870bbab4 2012/09/09 20:06:34 pgurumur $\n"

      fh = None
      try:
         fh = open(self._path, "w")
      except IOError as err:
         self._error(err)
      else:
         fh.write(content)
         fh.close()

      directory = os.path.dirname(self._path)
      if self._isNew:
         self._svn.add(self._path)
         self._svn.propset("svn:keywords", "Id", self._path)

      self._svn.checkin(directory, Comment)


   def Update(self):
      directory = os.path.dirname(self._path)
      changes = self._svn.status(directory, True, False, True)
      for ch in changes:
         if ch.text_status == pysvn.wc_status_kind.conflicted:
            self._log("Working copy %s in conflict", ch.path)
            self._svn.revert(ch.path)
            self._svn.update(ch.path)

         if ch.repos_text_status == wc_status_kind.modified:
            self._svn.update(ch.path)

   def GetCurrent(self):
      res = None

      fh = None
      try:
         fh = file(self._path)
         res = fh.read()
      except IOError, message:
         pass

      if fh is not None:
         fh.close()

      return res

   def Remove(self):
      directory = os.path.dirname(self._path)
      shutil.rmtree(directory)

# Subversion class
class svn(object):
   def __init__(self, **Keywords):
      super(svn, self).__init__()
      """
      constructor takes key value pairs as optional arguments
      example:
         scm = svn()
         or
         scm = svn(username = "user", password = "password", log = "logfile")
         or
         scm = svn(username = "user", password = "password")
      """

      # SVN client object
      self._client = None

      # Username and password vars
      self._username = None
      self._password = None

      if "username" in Keywords:
         self._username = Keywords["username"]

      if "password" in Keywords:
         self._password = Keywords["password"]

      if "log" in Keywords:
         if bool(Keywords["log"]) is True:
            self._log = log.StreamLog(stdlib.my_name(self))
         else:
            self._log = log.NullLog(stdlib.my_name(self))

      self._log_message = "default upload"
      self._revUpdateComplete = None
      self.loginCount = 0

   # Instantiate the pysvn client object with all the necessary callbacks
   def Client(self):
      self._client = pysvn.Client()
      self._client.exception_style = 1
      self._client.callback_get_login = self._GetLogin
      self._client.callback_get_log_message = self._GetLogMessage
      self._client.callback_notify = self._Notify
      self._client.callback_cancel = self._Cancel
      self._client.callback_ssl_client_cert_password_prompt = \
            self._CallbackSSLPasswordPrompt
      self._client.callback_ssl_client_cert_prompt = \
            self._CallbackSSLClientPrompt
      self._client.callback_ssl_server_prompt = self._CallbackSSLServerPrompt
      self._client.callback_ssl_server_trust_prompt = \
            self._CallbackSSLServerTrustPrompt

   def _error(self, Message):
      if Message:
         raise SCMError(Message)

   def _GetLogin(self, Realm, User, Save):
      if self._username:
         self.loginCount += 1
         if self.loginCount > 10:
            return False, '', '', False

      if self._username:
         return True, self._username, self._password, False
      else:
         return False, '', '', False

   def CheckIn(self, File, Comment, New = False):
      if os.path.exists(File):
         if New:
            self.add(File)
            self.propset("svn:keywords", "Id", File)

         self.checkin(File, Comment)

   def Update(self, Path):
      retval = None
      temp = None
      changes = self.status(Path, True, False, True)
      if changes:
         for ch in changes:
            if ch.text_status == pysvn.wc_status_kind.conflicted:
               self.revert(ch.path)
               temp = self.update(ch.path)

            if ch.repos_text_status == pysvn.wc_status_kind.modified:
               temp = self.update(ch.path)
      else:
         temp = self.update(Path)
         retval = temp[0].number

      return int(retval)

   def _GetLogMessage(self):
      return True, self._log_message

   def _Cancel(self):
      return False

   def _Notify(self, ArgDict):
      if ArgDict['action'] == pysvn.wc_notify_action.update_completed:
         self._revUpdateComplete = ArgDict['revision']
      elif ArgDict['path']:
         msg = 'svn: %s %s' % (ArgDict['action'], ArgDict['path'])
         self._log(msg)

   def exists(self, URL):
      retval = True
      if URL:
         try:
            handle(self._client.info2, URL)
         except :
            retval = False

      return retval

   def WorkingCopy(self, Path, Url):
      if Path and URL:
         try:
            handle(self._client.mkdir, URL, "new directory")
         except SVNErrorMethodNotAllowed:
            pass
         except SVNErrorNotInRepo:
            raise
         else:
            self.checkout(Path, URL)

   def checkout(self, Path, URL):
      retval = 0
      if Path and URL:
         self._revUpdateComplete = None
         try:
            handle(self._client.checkout, URL, Path, recurse=True)
         except SVNErrorNotInRepo as err:
            self._error(err)
         except SVNErrorAuthFailed as err:
            self._error(err)
         except SVNErrorNotWC as err:
            self._error(err)
         else:
            if self._revUpdateComplete is not None:
               retval = self._revUpdateComplete.number

      return retval

   def add(self, Path):
     if Path:
        self._client.add(Path, recurse = True, force = False)

   def checkin(self, Path, Mesg = None):
      retval = None
      if Path:
         msg = None
         if Mesg is None:
            ix, msg = self._GetLogMessage()
         else:
            msg = Mesg

         rev = self._client.checkin(Path, msg, recurse = True)
         if rev is None:
            retval = 0
         elif rev.number > 0:
            retval = rev.number
         else:
            self._error("SVN commit failed")

      return retval

   def update(self, Path):
      return self._client.update(Path, recurse = True)

   def list(self, Path, recurse = False):
      retval = []
      for ix in self._client.list(Path, recurse = recurse):
         retval.append(List(**ix[0].data))

      return retval

   def info2(self, Path, recurse = False):
      (path, data) = self._client.info2(Path, recurse = recurse)[0]
      return (path, data)

   def propset(self, Name, Value, Path, recurse = False):
      self._client.propset(Name, Value, Path, recurse = recurse)

   def remove(self, PathList):
      self._client.remove(PathList, force = True)

   def revert(self, Path):
      return self._client.revert(Path)

   def status(self, Path, recurse = True, getall = True, update = False):
      return self._client.status(Path, recurse, getall, update)

   def _CallbackSSLPasswordPrompt(self):
      pass

   def _CallbackSSLClientPrompt(self):
      pass

   def _CallbackSSLServerPrompt(self):
      pass

   def _CallbackSSLServerTrustPrompt(self, Data):
      return True, Data['failures'], True

class mercury(object):
   def __init__(self, URL, User = None):
      super(QuickSilver, self).__init__()
      self._user = None
      self._url = None
      self._dir = None
      self._ui = ui.ui()

      if User:
         self._user = User
      else:
         self._user = self._ui.username()

      if URL:
         self._url = URL

      if self._ui.has_section('paths'):
         for path in self._ui.configitems('paths'):
            if path[0] == self._url:
               self._url = path[1]
               break

      if self._url:
         self._repo = self._hgutil(self._ui, self._url)

   def _error(self, fmt, *args):
      errmesg = None
      if len(args):
         errmesg = stdlib.sprintf(fmt, *args)
      else:
         errmesg = stdlib.my_str(fmt)

      raise SCMError(errmesg)

   def _expandfiles(self, files = []):
      listFiles = []
      pwd = os.getcwd()
      for ix in files:
         temp = "%s/%s" %(pwd, ix)
         listFiles.append(temp)

      return listFiles

   def _hgutil(self, UI, Directory, Change = False):
      retval = None
      try:
         if Change and Directory and os.path.exists(Directory):
            os.chdir(Directory)
      except OSError as err:
         self._error(err)

      try:
         retval = hg.repository(UI, Directory)
      except error.RepoError as err:
         self._error(err)

      return retval

   def addDir(self, Directory):
      self._dir = Directory

   def checkout(self):
      try:
         if self._dir:
            commands.clone(self._ui, self._url, self._dir)
         else:
            self._error("No directory given to checkout the repository")
      except error.Abort as err:
         self._error(err)

   def commit(self, Message, files = []):
      opts = {}
      opts['user'] = self._user
      if Message:
         opts['message'] = stdlib.my_str(Message)
      else:
         self._error("Cannot commit to repository without checkin message!")

      if len(files):
         files = self._expandfiles(files)

      if not self._dir:
         path = util.expandpath(util.urllocalpath(os.getcwd()))
         repo = self._hgutil(self._ui, cmdutil.findrepo(path))

      if self._url:
         if Message:
            commands.commit(self._ui, repo, *files, **opts)

         commands.push(self._ui, repo, self._url)
      else:
         self._error("Cannot proceed to commit without hg/mercurial URL")

   def update(self):
      if self._url:
         repo = None
         if self._dir:
            repo = self._hgutil(self._ui, self._dir)
         else:
            path = util.expandpath(util.urllocalpath(os.getcwd()))
            repo = self._hgutil(self._ui, cmdutil.findrepo(path))

         commands.pull(self._ui, repo, self._url)
         commands.update(self._ui, repo)
      else:
         self._error("Cannot proceed to update without hg/mercurial URL")

   def add(self, files = []):
      if len(files):
         files = self._expandfiles(files)

      if not self._dir:
         path = util.expandpath(util.urllocalpath(os.getcwd()))
         repo = self._hgutil(self._ui, cmdutil.findrepo(path))

      if len(files):
         commands.add(self._ui, repo, *files)

   def summary(self):
      repo = None
      if self._dir:
         repo = self._hgutil(self._ui, self._dir)
      else:
         path = util.expandpath(util.urllocalpath(os.getcwd()))
         repo = self._hgutil(self._ui, cmdutil.findrepo(path))

      commands.summary(self._ui, repo)
